/****************************************************************************
This is based on the DirectShow.Net sample, PlayWnd
*****************************************************************************/

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

using DirectShowLib;
using DirectShowLib.Utils;
using DirectShowLib.DMO;
using System.IO;
using System.Threading;
using System.Diagnostics;

namespace HovisPresent
{
	internal enum PlayState
	{
		Stopped,
		Paused,
		Running,
		Init
	};

	public class Perf
	{
		private int start;
		private string text;

		public Perf(string t) {
#if DEBUG
			text = t;
			start = Environment.TickCount;
#endif
		}
		public void Time() {
#if DEBUG
			Console.WriteLine("{0} : {1}ms", text, Environment.TickCount - start);
#endif
		}
	}

	public enum DSVideoMessageStatus
	{
		Complete,
		Error,
		Message
	}

	public class DSVideoMessageEventArgs : EventArgs	{

		private string _message;
		private Exception _exception;
		private string _source;
		private DSVideoMessageStatus _status;

		public string Message { get { return _message; } }
		public Exception Exception { get { return _exception; } }
		public string Source { get { return _source; } }
		public DSVideoMessageStatus Status { get { return _status; } }

		public string FullMessage {
			get {
				string m = _message == null ? "" : _message;
				m += ":";

				if (_exception != null) {
					m += _exception.Message + "\r\n";
					string[] splitStack = _exception.StackTrace.Split(new char[] { '\n' }, 2);
					if (splitStack.Length>=2)
						m += "\r\n" + splitStack[0] + "\r\n"+ splitStack[1];
				}

				return m;
			}
		}

		public DSVideoMessageEventArgs(DSVideoMessageStatus status, string message, string source, Exception exception) {
			_message = message;
			_exception = exception;
			_status = status;
			_source = source;
		}
	}

	public delegate void DSVideoMessageEventHandler(object sender, DSVideoMessageEventArgs e);


	public class DSVideo : Control
	{
		private const int WMGraphNotify = 0x0400 + 13;
		private const int VolumeFull = 0;
		private const int VolumeSilence = -10000;

		private IGraphBuilder graphBuilder = null;
		private IMediaControl mediaControl = null;
		private IMediaSeeking mediaSeeking = null;
		private IMediaEventEx mediaEventEx = null;
		private IVideoWindow videoWindow = null;
		private IBasicAudio basicAudio = null;
		private IBasicVideo basicVideo = null;
		private IVMRFilterConfig vmrFilterConfig = null;

		IBaseFilter reader = null;
		IBaseFilter vdecoder = null;
		IBaseFilter adecoder = null;
		VideoMixingRenderer vrender = null;
		IVMRMonitorConfig monitorConfig = null;
		IBaseFilter arender = null;

		IPin	pinReaderVOut = null,
				pinReaderAOut = null,
				pinVDecoderIn = null,
				pinVDecoderOut = null,
				pinADecoderIn = null,
				pinADecoderOut = null,
				pinVRenderIn = null,
				pinARenderIn = null;

		/// <summary>
		/// Set to true if graph building, playing, interface getting is allOK (i.e. no errors when playing a file)
		/// </summary>
		private bool allOK;
		private bool useAudio;
		private bool fileFilterAdded = false;
		private IntPtr hDrain = IntPtr.Zero;
		private int monitor;
		private string currentFilename;

#if DEBUG
		private DsROTEntry rot = null;
#endif

		private System.ComponentModel.Container components = null;

		public event DSVideoMessageEventHandler DSVideoMessage;

		public DSVideo(bool audio)  {
			useAudio = audio;
			allOK = false;
			InitializeComponent();
		}

		protected override void Dispose(bool disposing) {
			if (disposing) {
				if (components != null) {
					components.Dispose();
				}
			}
			base.Dispose(disposing);
		}

		public void SignalError(string m,Exception e) {
			if (DSVideoMessage != null)
				DSVideoMessage(this, new DSVideoMessageEventArgs( DSVideoMessageStatus.Error,m,Name,e) );
		}

		public void SignalStatus(DSVideoMessageStatus s, string message) {
			if (DSVideoMessage != null)
				DSVideoMessage(this, new DSVideoMessageEventArgs(s, message, Name, null));
		}

		public void SignalMessage(string m) {
			if (DSVideoMessage!=null)
				DSVideoMessage(this, new DSVideoMessageEventArgs(DSVideoMessageStatus.Message,m,Name,null) );
		}

		private void InitializeComponent() {
			ClientSize = new System.Drawing.Size(240, 120);
			Resize += new System.EventHandler(DSVideo_MoveResize);
			Move += new System.EventHandler(DSVideo_MoveResize);
			ParentChanged+=new EventHandler(DSVideo_ParentChanged);
		}

		void DSVideo_ParentChanged(object sender, EventArgs e) {
			((Form)Parent).Closing += new CancelEventHandler(DSVideo_Closing);
		}

		void DSVideo_Closing(object sender, CancelEventArgs e) {
			CloseInterfaces();
		}

		private bool BuildGraph() {
			try {
				fileFilterAdded=false;

				Guid vDecoderGuid = new Guid("82d353df-90bd-4382-8bc2-3f6192b76e34"); // WMVideo DMO
				Guid vRendererGuid = new Guid("B87BEB7B-8D29-423F-AE4D-6582C10175AC");
				Guid aDecoderGuid = new Guid("2EEB4ADF-4578-4D10-BCA7-BB955F56320A"); // WMAudio DMO
				Guid aRendererGuid = new Guid("79376820-07D0-11CF-A24D-0020AFD79767");

				IDMOWrapperFilter vdecoderDMOWrapper = (IDMOWrapperFilter)new DMOWrapperFilter();
				vdecoderDMOWrapper.Init(vDecoderGuid, DirectShowLib.DMO.DMOCategory.VideoDecoder);
				vdecoder = (IBaseFilter)vdecoderDMOWrapper;
				graphBuilder.AddFilter(vdecoder, "WMV DMO Decoder");

				vrender = new VideoMixingRenderer();
				graphBuilder.AddFilter(vrender as IBaseFilter, "Video Renderer");
				monitorConfig = (IVMRMonitorConfig)vrender;
				setMonitorInternal();

				if (useAudio) {
					IDMOWrapperFilter adecoderDMOWrapper = (IDMOWrapperFilter)new DMOWrapperFilter();
					adecoderDMOWrapper.Init(aDecoderGuid, DirectShowLib.DMO.DMOCategory.AudioDecoder);
					adecoder = (IBaseFilter)adecoderDMOWrapper;
					graphBuilder.AddFilter(adecoder, "WMV DMO Decoder");

					arender = FilterGraphTools.AddFilterFromClsid(graphBuilder, aRendererGuid, "arenderer");

					pinADecoderIn = DsFindPin.ByName(adecoder, "in0");
					pinADecoderOut = DsFindPin.ByName(adecoder, "out0");
					pinARenderIn = DsFindPin.ByName(arender, "Audio Input pin (rendered)");
				}

				pinVDecoderIn = DsFindPin.ByName(vdecoder,"in0");
				pinVDecoderOut = DsFindPin.ByName(vdecoder,"out0");
				pinVRenderIn = DsFindPin.ByName(vrender as IBaseFilter, "VMR Input0");
				return true;
			} catch (Exception e) {
				SignalError("Fatal Error Building Initial Graph", e);
				return false;
			}
		}

		/// <summary>
		/// Sets the monitor index that will be displayed on
		/// </summary>
		/// <param name="n">The n.</param>
		public void SetMonitor(int n) {
			monitor = n;
			if (allOK && monitorConfig != null)
				setMonitorInternal();
		}

		/// <summary>
		/// Sets the monitor
		/// </summary>
		private void setMonitorInternal() {
			VMRMonitorInfo[] monitors = new VMRMonitorInfo[10];
			int numMonitors;
			monitorConfig.GetAvailableMonitors(monitors, 10, out numMonitors);

			if (numMonitors == 1 || monitor >= numMonitors)
				return;

			monitorConfig.SetMonitor(ref monitors[monitor+1].guid);

		}

		/// <summary>
		/// Disconnects the pin if its not null
		/// </summary>
		/// <param name="pin">The pin.</param>
		private void DisconnectPin(IPin pin) {
			if (pin != null) {
				int hr = graphBuilder.Disconnect(pin);
				DsError.ThrowExceptionForHR(hr);
			}
		}

		private void DisconnectPins() {
			try {
				int hr;
				if (fileFilterAdded) {
					DisconnectPin(pinReaderVOut);
					DisconnectPin(pinVDecoderIn);
					DisconnectPin(pinVDecoderOut);

					if (useAudio) {
						DisconnectPin(pinReaderAOut);
						DisconnectPin(pinADecoderIn);
						DisconnectPin(pinADecoderOut);
						DisconnectPin(pinARenderIn);
					}

					hr = graphBuilder.Disconnect(pinVRenderIn);
					hr = graphBuilder.RemoveFilter(reader);
					Marshal.ReleaseComObject(reader);
					reader = null;
				}
			} catch (Exception e) {
				SignalError("Error disconnecting pins", e);
			}
		}

		private bool AddPlayFilter(string filename) {
			int hr;
			try {
				DisconnectPins();

				// add a new file reader object
				WMAsfReader readerObject = new WMAsfReader();
				((IFileSourceFilter)readerObject).Load(filename, null);
				reader = (IBaseFilter)readerObject;
				graphBuilder.AddFilter(reader, "Reader");

				pinReaderVOut= DsFindPin.ByName(reader,"Raw Video 1");
				if (pinReaderVOut==null)
					pinReaderVOut = DsFindPin.ByName(reader, "Raw Video 0");

				if (useAudio) {
					pinReaderAOut = DsFindPin.ByName(reader, "Raw Audio 0");
					if (pinReaderAOut == null)
						pinReaderAOut = DsFindPin.ByName(reader, "Raw Audio 1");
				}

				Debug.WriteLine("Connecting VOut to VDecoderIn");
				hr = graphBuilder.ConnectDirect(pinReaderVOut, pinVDecoderIn, null);
				DsError.ThrowExceptionForHR(hr);
				Debug.WriteLine("Connecting VDecoderOut to VRenderIn");
				hr = graphBuilder.ConnectDirect(pinVDecoderOut, pinVRenderIn, null);
				DsError.ThrowExceptionForHR(hr);

				if (useAudio) {
					if (pinReaderAOut != null) {
						Debug.WriteLine("Connecting ReaderAout to ADecoderIn");
						hr = graphBuilder.ConnectDirect(pinReaderAOut, pinADecoderIn, null);
						DsError.ThrowExceptionForHR(hr);
						Debug.WriteLine("Connecting ADecoderOut to ARenderIn");
						hr = graphBuilder.ConnectDirect(pinADecoderOut, pinARenderIn, null);
						DsError.ThrowExceptionForHR(hr);
					}
				}

				fileFilterAdded=true;
				return true;
			} catch (Exception e) {
				SignalError("Error opening video", e);
				return false;
			}
		}

		private void PlayMovieInWindow(string filename) {
			int hr = 0;
			allOK = false;

			if (filename == string.Empty)
				return;


			Perf build = new Perf("Build graph");

#if DEBUG
			if (rot != null) {
				rot.Dispose();
				rot = null;
			}
#endif

			if (graphBuilder == null) {
				graphBuilder = (IGraphBuilder)new FilterGraph();
				if (!BuildGraph())
					return;
			}

			if (!AddPlayFilter(filename))
				return;

			build.Time();

#if DEBUG
			rot = new DsROTEntry(graphBuilder);
#endif

			Perf stuff = new Perf("stuff");

			if (mediaControl == null) {
				if (!GetControlInterfaces())
					return;
			}
			allOK = true;

			stuff.Time();

			MoveVideoWindow();

			Perf run = new Perf("Run");
			hr = mediaControl.Run();
			currentFilename = filename;
			run.Time();

			if (hr < 0) {
				allOK = false;
				// It seems we don't need to do this, graph notify gets an ErrorAbort status
				// so instead of sending twice and having to filter the message for dupes
				// ignore this error
				//SignalError( "Error Playing Video", new COMException(DsError.GetErrorText(hr),hr ));
			}

			ShowVideoWindow(true);
		}

		private bool GetControlInterfaces() {
			try {
				int hr;
				// QueryInterface for DirectShow interfaces
				mediaControl = (IMediaControl)graphBuilder;
				mediaEventEx = (IMediaEventEx)graphBuilder;
				mediaSeeking = (IMediaSeeking)graphBuilder;

				videoWindow = graphBuilder as IVideoWindow;
				basicVideo = graphBuilder as IBasicVideo;

				vmrFilterConfig = vrender as IVMRFilterConfig;
				if (vmrFilterConfig != null) {
					vmrFilterConfig.SetRenderingPrefs(VMRRenderPrefs.AllowOffscreen);
					vmrFilterConfig = null;
				}

				basicAudio = graphBuilder as IBasicAudio;

				// Have the graph signal event via window callbacks for performance
				hr = mediaEventEx.SetNotifyWindow(Handle, WMGraphNotify, IntPtr.Zero);
				DsError.ThrowExceptionForHR(hr);

				// Setup the video window
				hr = videoWindow.put_Owner(Handle);
				DsError.ThrowExceptionForHR(hr);

				hr = videoWindow.put_WindowStyle(WindowStyle.Child | WindowStyle.ClipSiblings | WindowStyle.ClipChildren);
				DsError.ThrowExceptionForHR(hr);
				return true;
			} catch (Exception e) {
				SignalError("Error getting control interfaces", e);
				return false;
			}
		}

		private void CloseClip() {
			StopWithWait();
		}

		private void MoveVideoWindow() {
			if (!allOK)
				return;

			int hr = 0;

			Size scaledSize = GetScaledVidSize();

			// Track the movement of the container window and resize as needed
			if (videoWindow != null) {
				hr = videoWindow.SetWindowPosition(
				  ClientRectangle.Left + (ClientRectangle.Width-scaledSize.Width)/2,
				  ClientRectangle.Top + (ClientRectangle.Height-scaledSize.Height)/2,
				  scaledSize.Width,
				  scaledSize.Height
				  );
				if (hr != 0)
					SignalError("Error Resizing Video Window", null);
			}
		}

		private void CloseInterfaces() {
			int hr = 0;

			try {
				lock (this) {
					// Relinquish ownership (IMPORTANT!) after hiding video window
					ShowVideoWindow(false);
					hr = videoWindow.put_Owner(IntPtr.Zero);
					DsError.ThrowExceptionForHR(hr);

					if (mediaEventEx != null) {
						hr = mediaEventEx.SetNotifyWindow(IntPtr.Zero, 0, IntPtr.Zero);
						DsError.ThrowExceptionForHR(hr);
					}

#if DEBUG
					if (rot != null) {
						rot.Dispose();
						rot = null;
					}
#endif
//					DisconnectPins();

					// Release and zero DirectShow interfaces
					mediaEventEx = null;
					mediaControl = null;
					mediaSeeking = null;
					basicAudio = null;
					basicVideo = null;
					videoWindow = null;
					vmrFilterConfig = null;
					monitorConfig = null;

					pinReaderVOut = pinReaderAOut =
					pinVDecoderIn = pinVDecoderOut =
					pinADecoderIn = pinADecoderOut =
					pinVRenderIn = pinARenderIn = null;

					if (reader != null)
						Marshal.ReleaseComObject(reader);
					reader= null;
					if (vdecoder != null)
						Marshal.ReleaseComObject(vdecoder);
					vdecoder= null;
					if (adecoder != null)
						Marshal.ReleaseComObject(adecoder);
					adecoder= null;
					if (vrender != null)
						Marshal.ReleaseComObject(vrender);
					vrender= null;
					if (arender != null)
						Marshal.ReleaseComObject(arender);
					arender = null;
					if (graphBuilder != null)
						Marshal.ReleaseComObject(graphBuilder); 
					graphBuilder = null;

					GC.Collect();
				}
			} catch {
			}
		}

		const int E_FAIL = unchecked((int)0x80004005);
		const int VFW_S_STATE_INTERMEDIATE = 0x00040237;

		//
		// Tell the graph to stop
		public void Stop() {
			if (!allOK)
				return;
			ShowVideoWindow(false);
			if (mediaControl == null)
				return;
			mediaControl.Stop();
		}


		//
		// Tell the graph to stop and wait for it to confirm
		public void StopWithWait() {
			int hr = 0;

			if (!allOK)
				return;

			ShowVideoWindow(false);

			if (mediaControl == null)
				return;

			hr = mediaControl.Stop();
			while( true ) {
				
				FilterState state;
				hr = mediaControl.GetState(15, out state);

				if (hr <0 ) {
					IndicateFailure("mediaControl.GetState", hr);
					break;
				}

				if (hr==0 && state== FilterState.Stopped)
					break;
			}
		}

		private void IndicateFailure(string s) {
			MessageBox.Show(s);
		}

		private void IndicateFailure(string s,int hr) {
			string message = String.Format("{0} - {1:x}",s,hr);
			MessageBox.Show(message);
		}

		//
		//
		//
		private void HandleGraphEvent() {
			int hr = 0;
			EventCode evCode;
			IntPtr evParam1, evParam2;

			// Make sure that we don't access the media event interface
			// after it has already been released.
			if (mediaEventEx == null)
				return;

			// Process all queued events
			while (mediaEventEx.GetEvent(out evCode, out evParam1, out evParam2, 0) == 0) {

				Debug.WriteLine( "Event(" + evCode + ")");

				switch (evCode) {
					case EventCode.ErrorAbort:
						SignalStatus(DSVideoMessageStatus.Error,"Error playing video");
						break;

					case EventCode.Complete:
						ShowVideoWindow(false);
						SignalStatus(DSVideoMessageStatus.Complete,"Video playback complete");
						break;
				}

				// Free memory associated with callback, since we're not using it
				hr = mediaEventEx.FreeEventParams(evCode, evParam1, evParam2);
			}
		}

		public void PlayFile(string filename) {
			if (this.currentFilename == filename) {
				mediaControl.Stop();
				DsLong currentPos = 0;
				DsLong stopPos = 0;
				mediaSeeking.SetPositions(currentPos, AMSeekingSeekingFlags.AbsolutePositioning, stopPos, AMSeekingSeekingFlags.NoPositioning);
				mediaControl.Run();
				return;
			}
			CloseClip();

			// Open the new clip
			PlayMovieInWindow(filename);
		}


		/*
		 * WinForm Related methods
		 */

		protected override void WndProc(ref Message m) {
			switch (m.Msg) {
				case WMGraphNotify: {
						HandleGraphEvent();
						break;
					}
			}

			// Pass this message to the video window for notification of system changes
			if (videoWindow != null)
				videoWindow.NotifyOwnerMessage(m.HWnd, m.Msg, m.WParam, m.LParam);

			base.WndProc(ref m);
		}

		private void DSVideo_MoveResize(object sender, System.EventArgs e) {
			MoveVideoWindow();
		}

		private void MainForm_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
			StopWithWait();
			CloseInterfaces();
		}

		// 
		// Get size of video scaled to window size in correct Aspect ratio
		//
		private Size GetScaledVidSize() {
			if (basicVideo == null)
				return new Size(1000, 100);

			int vWidth,vHeight;
			basicVideo.get_VideoWidth(out vWidth);
			basicVideo.get_VideoHeight(out vHeight);

			// AR of video
			double vAR = ((double)vWidth) / ((double)vHeight);			

			// AR of window
			double wAR = ((double)Width) / ((double)Height);

			// if window AR > video AR, window is wider, so scale to height
			// else scale to width

			if (wAR > vAR) {
				return new Size( (int) (Height * vAR), Height);
			} else {
				return new Size(Width, (int) (Width/vAR));
			}
		}


		//
		// Show/Hide the actual video window
		//
		public void ShowVideoWindow(bool visible) {
			try {
				int hr;
				if (videoWindow != null) {
					hr = videoWindow.put_Visible(visible ? OABool.True : OABool.False);
					DsError.ThrowExceptionForHR(hr);
				}
			}  catch (Exception e) {
				SignalError("Cannot Show Video", e);
			}
		}

	}
}
